package chatserver.objects;

import chatserver.sql.MySqlConnection;
import chatserver.container.AliasInfo;
import chatserver.container.AutoOpInfo;
import chatserver.container.BanInfo;
import chatserver.netio.AttrList;
import chatserver.netio.EventHandler;
import chatserver.logging.ServerLog;
import chatserver.netio.Command;
import chatserver.netio.Connection;
import chatserver.netio.ClientAttr;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Collections;
import java.util.logging.*;

public class Server {

    private Thread publicSocketThread;
    private ServerSocket socket;
    private List<Connection> connections;
    private ServerConfig config;
    private List<User> connectedUsers;
    private Map<String, Channel> openChannels;
    private Map<String, AliasInfo> connectedUsersPerIp;
    private MySqlConnection mySql;

    private Server() {
        this.config = new ServerConfig();

        this.connections = Collections.synchronizedList(new ArrayList<Connection>());
        this.connectedUsers = Collections.synchronizedList(new ArrayList<User>());
        this.openChannels = Collections.synchronizedMap(new HashMap<String, Channel>());
        this.connectedUsersPerIp = Collections.synchronizedMap(new HashMap<String, AliasInfo>());

        try {
            this.socket = new ServerSocket(config.getPort(), 64, InetAddress.getByName(config.getHost()));
        } catch (IOException ex) {
            Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
        }

        final Server myself = this;
        Runnable r1 = new Runnable() {

            @Override
            public void run() {
                srv.PublicSocketLoop();
            }
            private final Server srv;

            {
                srv = myself;
            }
        };
        this.publicSocketThread = new Thread(r1);

        this.mySql = MySqlConnection.getInstance();
    }
    private static Server myInst = null;

    public static Server getInstance() {
        if (myInst == null) {
            myInst = new Server();
        }
        return myInst;
    }

    public void start() {
        this.publicSocketThread.start();
    }

    public int getChatterCount() {
        return this.connectedUsers.size();
    }

    public void loadStaticChannels() {
        List<String> sNames = mySql.getStaticChannels();

        for (String name : sNames) {
            if (openChannels.containsKey(name)) {
                continue;
            }
            openChannels.put(name, new Channel(name, name, true, false, Channel.Profile.DEFAULT, config.getDefaultSlotsPerChannel(), "", "", ""));
        }
    }

    private void PublicSocketLoop() {

        loadStaticChannels();

        long lastTimeoutCheck = 0;
        while (true) {

            // Accept new connections
            Socket connectionSocket;
            try {
                connectionSocket = socket.accept();
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                continue;
            }

            BufferedInputStream inFromClient;
            try {
                inFromClient = new BufferedInputStream(connectionSocket.getInputStream());
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                continue;
            }

            BufferedOutputStream outToClient;
            try {
                outToClient = new BufferedOutputStream(connectionSocket.getOutputStream());
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                continue;
            }
            try {
                Connection con = new Connection(connectionSocket, inFromClient, outToClient);
                this.connections.add(con);
                ServerLog.log(con, "connected");

                AttrList at = new AttrList();
                at.safeAddClientAttr(ClientAttr.DIGEST_SALT, "empty");
                at.safeAddClientAttr(ClientAttr.CHALLENGE, "chatclient.applets.frames.ChallengeeID");
                con.sendCommand(new Command(Command.CHALLENGE, at));
            } catch (Exception ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
            }

            // Check for timeout on connections
            long now = new Date().getTime();
            if (now - lastTimeoutCheck > 1000) {
                CheckConnectionTimeout();
                lastTimeoutCheck = new Date().getTime();
            }
        }
    }

    public void CheckConnectionTimeout() {
        long now = new Date().getTime();
        for (Connection c : connections) {
            if (c.getEventHandler().getConnectionStatus() == EventHandler.ConnectionStatus.CONNECTED
                    && now - c.getConnectionTime() > 30000) {
                ServerLog.log(c, "connection timeout");
                c.close();
            }
        }
    }

    private boolean removePublicConnection(Connection con) {
        if (this.connections.contains(con)) {
            this.connections.remove(con);
            removeIpSlot(con.getClientIp());
            return true;
        }
        return false;
    }

    public void sendToConnectedUsers(Command cmd) {
        for (User m : this.connectedUsers) {
            try {
                m.getConnection().sendCommand(cmd);
            } catch (Exception ex) {
            }
        }
    }

    public ServerConfig getConfig() {
        return config;
    }

    public void sendToConnectedUsers(Command cmd, User exclude) {
        for (User m : this.connectedUsers) {
            try {
                if (!m.equals(exclude)) {
                    m.getConnection().sendCommand(cmd);
                }
            } catch (Exception ex) {
            }
        }
    }

    public boolean isNameRegistered(String name) {
        return mySql.isNameRegistered(name);
    }

    public boolean isNameInUse(String name) {

        name = name.toLowerCase();

        for (User m : this.connectedUsers) {
            if (m.getLowerCaseName().equals(name)) {
                return true;
            }
        }

        return false;
    }

    public boolean isServerSlotFree() {
        ServerLog.log("ADZ - Server.java->isServerSlotFree:connectedUsers.size()="+connectedUsers.size()+":config.getMaxConnections()="+config.getMaxConnections());
        return connectedUsers.size() < config.getMaxConnections();
    }

    public boolean isIpSlotFree(Connection connection) {
        ServerLog.log("ADZ - Server.java->isIpSlotFree");
        String clientIp = connection.getClientIp();
        if (!connectedUsersPerIp.containsKey(clientIp)) {
            return true;
        }

        Integer count = connectedUsersPerIp.get(clientIp).getConnectionCount();
        ServerLog.log("ADZ - Server.java->isIpSlotFree:count="+count+":config.getMaxConnectionsPerIp()="+config.getMaxConnectionsPerIp());
        return count < config.getMaxConnectionsPerIp();
    }

    private void addIpSlot(String clientIp) {

        if (!connectedUsersPerIp.containsKey(clientIp)) {
            connectedUsersPerIp.put(clientIp, new AliasInfo(clientIp));
        } else {
            connectedUsersPerIp.get(clientIp).increase();
        }
    }

    private void removeIpSlot(String clientIp) {

        if (!connectedUsersPerIp.containsKey(clientIp)) {
            return;
        } else {
            AliasInfo nfo = connectedUsersPerIp.get(clientIp);
            nfo.decrease();

            if (nfo.getConnectionCount() < 1) {
                connectedUsersPerIp.remove(clientIp);
                return;
            }
        }
    }

    public boolean isBanned(Connection connection, String name) {
        return config.isBanned(connection, name);
    }

    public void addBan(Connection connnection, String name, int ipSize, long expires, String banner) {
        config.addBan(connnection, name, ipSize, expires, banner);
    }

    public boolean delBanByName(String name) {
        return config.delBanByName(name);
    }

    public List<BanInfo> getBanList() {
        return config.getBanList();
    }

    public Iterator<User> getUserIterator() {
        return connectedUsers.iterator();
    }

    public Channel createChannel(User invoker, String channelName, String channelType) {

        String channelNameId = channelName.toLowerCase();

        if (openChannels.containsKey(channelNameId)) {
            Command cmd = new Command(Command.NOPERMISSION);
            invoker.getConnection().sendCommand(cmd);
            return null;
        }

        if (config.isChannelReserved(channelName)) {
            Command cmd = new Command(Command.NOPERMISSION);
            invoker.getConnection().sendCommand(cmd);
            return null;
        }

        Channel c = new Channel(channelNameId, channelName, false, false, Channel.Profile.DEFAULT, config.getDefaultSlotsPerChannel(), config.getDefaultTopic(), invoker.getName(), invoker);
        mySql.updateChannelRoleOfUser(c.getSqlId(), invoker.getSqlId(), config.getDefaultCreatorChannelRole().getSqlId(), "Server");
        openChannels.put(channelNameId, c);

        return c;
    }

    public boolean isNameReserved(String name, boolean authSignUp) {
        if (name.length() < 1 || name.equals("")) {
            return true;
        }

        if (config.isNameReserved(name)) {
            return true;
        }

        if (!authSignUp) {
            if (Character.valueOf(name.charAt(0)).toString().equals("~")) {
                return true;
            }
        }

        return false;
    }

    public boolean isNameLengthOk(String name) {
        int length = name.length();

        return length > 0 && length <= config.getMaxNickLength();
    }

    public boolean checkSignUpRequirements(Connection connection, String name, boolean authSignUp) {
        ServerLog.log("ADZ - Server.java->checkSignUpRequirements");

        if (!isServerSlotFree()) {
            ServerLog.log("ADZ - Server.java->checkSignUpRequirements:isServerSlotFree()=False");
            connection.sendCommand(new Command(Command.ENQUEUED));

            AttrList at = new AttrList();
            at.safeAddClientAttr(ClientAttr.QUEUE_SEQNR, 1);
            connection.sendCommand(new Command(Command.QUEUEPOSITION));

            at = new AttrList();
            at.safeAddClientAttr(ClientAttr.QUEUE_CURRENT, 22);
            connection.sendCommand(new Command(Command.QUEUEUPDATE));

            return false;
        } else if (!isIpSlotFree(connection)) {
            ServerLog.log("ADZ - Server.java->checkSignUpRequirements:isIpSlotFree(connection)=False");
            connection.sendCommand(new Command(Command.TOOMANYIPCXNS));
            return false;
        } else if (isBanned(connection, name)) {
            connection.sendCommand(new Command(Command.BANNEDONSERVER));
            return false;
        }

        if (!authSignUp) {
            if (isNameRegistered(name)) {
                AttrList at = new AttrList();
                Random r = new Random();
                String salt = Long.toString(Math.abs(r.nextLong()), 36);
                connection.setTmpLoginData(name, salt);
                at.safeAddClientAttr(ClientAttr.DIGEST_SALT, salt);
                at.safeAddClientAttr(ClientAttr.CHALLENGE, "ChallengeePassword");
                connection.sendCommand(new Command(Command.CHALLENGE, at));
                return false;
            } else if (!isNameLengthOk(name)) {
                connection.sendCommand(new Command(Command.NICKRESERVED));
                return false;
            } else if (isNameReserved(name, authSignUp)) {
                connection.sendCommand(new Command(Command.NICKRESERVED));
                return false;
            } else if (isNameInUse("~" + name)) {
                ServerLog.log("ADZ - Server.java->checkSignUpRequirements:isNameInUse(~)=True");
                connection.sendCommand(new Command(Command.NICKINUSE));
                return false;
            }
        }

        return true;
    }

    public List<AutoOpInfo> getAutoOpList() {
        return mySql.getServerAutoOpList();
    }

    public User signUpUserAsGuest(Connection connection, String name) {

        if (!checkSignUpRequirements(connection, name, false)) {
            return null;
        }

        return loginUserAnonymously(connection, name);
    }

    private User loginUserAnonymously(Connection con, String name) {
        name = "~" + name;

        User m = new User(name, con);
        m.setRole(config.getDefaultGlobalRole(), "Server", true, true);

        addUser(m);

        return m;
    }

    public String getAliasesOfUser(User u) {
        if (!connectedUsersPerIp.containsKey(u.getIp())) {
            return "";
        } else {
            AliasInfo nfo = connectedUsersPerIp.get(u.getIp());
            return nfo.getAliasesOfUser(u);
        }
    }

    public Iterator<Channel> getChannelIterator() {
        return openChannels.values().iterator();
    }

    private void addUser(User m) {
        Connection con = m.getConnection();
        AttrList at = new AttrList();
        at.safeAddClientAttr(ClientAttr.NICKNAME, m.getName());
        Command cmd = new Command(Command.ADDEDCHATTER, at);
        con.sendCommand(cmd);
        sendToConnectedUsers(cmd);

        connectedUsers.add(m);
        addIpSlot(con.getClientIp());
        addAlias(m);
    }

    public User findUser(String name) {

        name = name.toLowerCase();

        Iterator<User> iter = connectedUsers.iterator();
        while (iter.hasNext()) {
            User u = iter.next();
            if (u.getLowerCaseName().equals(name)) {
                return u;
            }
        }
        return null;
    }
    private static char[] lowercases = {
        '\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007',
        '\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017',
        '\020', '\021', '\022', '\023', '\024', '\025', '\026', '\027',
        '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037',
        '\040', '\041', '\042', '\043', '\044', '\045', '\046', '\047',
        '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057',
        '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067',
        '\070', '\071', '\072', '\073', '\074', '\075', '\076', '\077',
        '\100', '\141', '\142', '\143', '\144', '\145', '\146', '\147',
        '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157',
        '\160', '\161', '\162', '\163', '\164', '\165', '\166', '\167',
        '\170', '\171', '\172', '\133', '\134', '\135', '\136', '\137',
        '\140', '\141', '\142', '\143', '\144', '\145', '\146', '\147',
        '\150', '\151', '\152', '\153', '\154', '\155', '\156', '\157',
        '\160', '\161', '\162', '\163', '\164', '\165', '\166', '\167',
        '\170', '\171', '\172', '\173', '\174', '\175', '\176', '\177'};

    public static boolean startsWithIgnoreCase(String text, String start) {
        if (start == null || start.length() < 1) {
            return false;
        }

        if (text == null || text.length() < start.length()) {
            return false;
        }

        for (int i = 0; i < start.length(); i++) {
            char c1 = text.charAt(i);
            char c2 = start.charAt(i);
            if (c1 != c2) {
                if (c1 <= 127) {
                    c1 = lowercases[c1];
                }
                if (c2 <= 127) {
                    c2 = lowercases[c2];
                }
                if (c1 != c2) {
                    return false;
                }
            }
        }
        return true;
    }

    public static List<User> findUsers(Collection<User> list, String name, String profile, String[] userinfo) {
        List<User> found = new ArrayList<User>();

        String[] profiles = null;

        if (profile != null) {
            profile = profile.toLowerCase();
            profiles = profile.split(":");
        }

        Iterator<User> iter = list.iterator();
        while (iter.hasNext()) {
            User u = iter.next();

            if (profiles != null) {

                boolean matches = false;

                for (String p : profiles) {
                    if (p != null && u.getDecidingGlobalRole().getProfile().equals(p)) {
                        matches = true;
                    }
                }

                if (!matches) {
                    continue;
                }
            }

            if (name != null && name.length() > 0 && !startsWithIgnoreCase(u.getName(), name)) {
                continue;
            }

            String[] tUserInfo = u.getUserInfo();
            for (int i = 0; i < userinfo.length; i++) {

                String uinfo = userinfo[i];

                if (uinfo != null && uinfo.length() > 0 && !startsWithIgnoreCase(tUserInfo[i], uinfo)) {
                    continue;
                }
            }

            found.add(u);

        }
        return found;
    }

    public List<User> findUsers(String name, String profile, String[] userinfo) {
        return findUsers(connectedUsers, name, profile, userinfo);
    }

    public Channel findChannel(String name) {
        ServerLog.log("ADZ - Server.java->findChannel:name="+name);
        name = name.toLowerCase();

        //for (int i = 0; i < openChannels.size(); i++)
        ServerLog.log("ADZ - Server.java->findChannel:openChannels.size()=" + openChannels.size() + ":Collection="+openChannels.values());
        
        boolean foundChannel = false;
        
        for (int i = 0; i < openChannels.keySet().toArray().length; i++)
        {
            ServerLog.log("ADZ - channel_" + i + " = " + openChannels.keySet().toArray()[i]);
            if (openChannels.keySet().toArray()[i] != null && ((String)openChannels.keySet().toArray()[i]).equalsIgnoreCase(name))
            {
                foundChannel = true;
                ServerLog.log("ADZ - channel " + name + " has been found.");
            }
        }
        
        if (openChannels.containsKey(name)) {
            
            ServerLog.log("ADZ - channel " + name + " has been found.");
            return openChannels.get(name);
        }

        return null;
    }

    public User signUpUserWithPassword(Connection con, String name, String password) {
        ServerLog.log("ADZ - Server.java->signUpUserWithPassword:name="+name+"password="+password);
        if (!isNameRegistered(name)) {
            return signUpUserAsGuest(con, name);
        }

        if (!checkSignUpRequirements(con, name, true)) {
            return null;
        }

        User m = new User(name, con);

        if (!m.authenticate(name, password)) {
            AttrList at = new AttrList();
            Random r = new Random();
            String salt = Long.toString(Math.abs(r.nextLong()), 36);
            con.setTmpLoginData(name, salt);
            at.safeAddClientAttr(ClientAttr.DIGEST_SALT, salt);
            at.safeAddClientAttr(ClientAttr.CHALLENGE, "ChallengeePassword");
            at.safeAddClientAttr(ClientAttr.INDEX, 1);
            con.sendCommand(new Command(Command.CHALLENGE, at));
            return null;
        }

        if (isNameInUse(name)) {
            kickUser(name);
        }

        for (int i = 0; i <m.getUserInfo().length; i++)
            ServerLog.log(i+":"+m.getUserInfo()[i]);
        
        addUser(m);

        return m;
    }

    public User signUpUserWithDigest(Connection con, String name, byte[] digest, String digestSalt) {
        ServerLog.log("ADZ - Server.java->signUpUserWithDigest:name="+name+":digest="+digest+":digest="+digestSalt);
        if (!isNameRegistered(name)) {
            return signUpUserAsGuest(con, name);
        }

        if (!checkSignUpRequirements(con, name, true)) {
            return null;
        }

        User m = new User(name, con);

        if (!m.authenticate(name, digest, digestSalt)) {
            AttrList at = new AttrList();
            Random r = new Random();
            String salt = Long.toString(Math.abs(r.nextLong()), 36);
            con.setTmpLoginData(name, salt);
            at.safeAddClientAttr(ClientAttr.DIGEST_SALT, salt);
            at.safeAddClientAttr(ClientAttr.CHALLENGE, "ChallengeePassword");
            at.safeAddClientAttr(ClientAttr.INDEX, 1);
            con.sendCommand(new Command(Command.CHALLENGE, at));
            return null;
        }

        if (isNameInUse(name)) {
            kickUser(name);
        }

        // ADZ - 
        for (int i = 0; i <m.getUserInfo().length; i++)
            ServerLog.log("ADZ - " + i+":"+m.getUserInfo()[i]);
        
        addUser(m);

        return m;
    }

    public boolean kickUser(String name) {

        name = name.toLowerCase();

        for (User m : connectedUsers) {
            if (m.getLowerCaseName().equals(name)) {
                m.getConnection().close();
                return true;
            }
        }
        return false;
    }

    public boolean kickUser(User km) {

        if (connectedUsers.contains(km)) {
            km.getConnection().close();
            return true;
        }

        return false;
    }

    public void onChannelClose(Channel c) {
        if (openChannels.containsKey(c.getNameId())) {
            openChannels.remove(c.getNameId());
        }
    }

    private void addAlias(User u) {
        if (!connectedUsersPerIp.containsKey(u.getIp())) {
            return;
        } else {
            AliasInfo nfo = connectedUsersPerIp.get(u.getIp());
            nfo.addAlias(u);
        }
    }

    private void removeAlias(User u) {
        if (!connectedUsersPerIp.containsKey(u.getIp())) {
            return;
        } else {
            AliasInfo nfo = connectedUsersPerIp.get(u.getIp());
            nfo.removeAlias(u);
        }
    }

    public void onUserDisconnect(User u) {
        removeAlias(u);
        if (connectedUsers.contains(u)) {
            connectedUsers.remove(u);
        }
    }

    public void onConnectionClosed(Connection con) {
        ServerLog.log(con, "disconnected");
        removePublicConnection(con);
    }

    public void rehash() {
        config.rehash();
        mySql.rehash();

        Iterator<Channel> iter = openChannels.values().iterator();
        while (iter.hasNext()) {
            Channel c = iter.next();
            c.rehash();
        }

        Iterator<User> iter2 = connectedUsers.iterator();
        while (iter2.hasNext()) {
            User u = iter2.next();
            u.rehash();
        }
    }
}
